package logic;

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Vector;

import data.*;
import data.GeneralDefinitions.OrderingParameters;
import java.math.BigDecimal;

public class ConfigurationAlgorithm extends Algorithm {

    private ConfigurationInput in;
    private double priceLim;
    private double powerLim;
    private double perfLim;
    private int amountSSD, amountSCSI, amountSATA;
    private double[][] iokbs ;
    private double[][] ionums ;
    private double[] sizes ;
    private int tustart;
    private int tuend;
    

    @Override
    public GraphData compute(AlgorithmInput input, Data data) {
        System.out.println("compute configuration started: " + GregorianCalendar.getInstance().getTime());
        in = (ConfigurationInput) input;
        priceLim = in.get_priceLimit();
        powerLim = in.get_powerConsumptionLimit();
        perfLim = in.get_performanceLimit();
        Vector<ConfigurationGraphObject> vecResult = new Vector<ConfigurationGraphObject>();
        int[] sortedLUNs = data.getOrderedLUNVector(in.get_orderingParameter());
        memoization(data, sortedLUNs,in.get_orderingParameter(), in.get_dateStart(), in.get_dateEnd());
       if(tustart==-1)
    	   return new ConfigurationGraphData(new Vector<ConfigurationGraphObject>());
        
        //computing all scsi configuration for later checks.
        amountSATA = 0;
        amountSSD = 0;
        amountSCSI = getMaxDisks(0,Data.SCSIDiskSize);
        double scsiPer = computePerformance(data, 0, sortedLUNs.length , sortedLUNs, in.get_orderingParameter());
        double scsiPrice = computePrice(amountSATA, amountSCSI, amountSSD, data);
        double scsiPower = computePowerConsumption(amountSATA, amountSCSI, amountSSD, data);

        int ssdLunIndex = 0;        
        int maxSSD = getMaxDisks(0,Data.SSDDiskSize);
        
        for(int ssdDisks=0; ssdDisks<=maxSSD; ssdDisks++){
        	ssdLunIndex=0;
        	//calculating the ssd last index
        	while(ssdLunIndex<sizes.length && sizes[ssdLunIndex]<ssdDisks*Data.SSDDiskSize){        		
        		ssdLunIndex++;
        	}
        	int scsiLunIndex=ssdLunIndex;
        	int maxSCSI = getMaxDisks(scsiLunIndex,Data.SCSIDiskSize);        
        	for(int scsiDisks=0; scsiDisks<=maxSCSI; scsiDisks++){
        		//calculating the scsi last index
        		while(scsiLunIndex<sizes.length && (sizes[scsiLunIndex]-sizes[ssdLunIndex])<scsiDisks*Data.SCSIDiskSize){        		
            		scsiLunIndex++;
            	}        		
        		//preparing the data for the computations
        		 amountSSD = ssdDisks;
                 amountSCSI = scsiDisks;
                 amountSATA = getMaxDisks(scsiLunIndex,Data.SATADiskSize);
                 
                 //computing initial results in order to save computations
                 double tmpPrice = computePrice(amountSATA, amountSCSI, amountSSD, data);
                 double tmpPower = computePowerConsumption(amountSATA, amountSCSI, amountSSD, data);                 
                 
                 if (tmpPrice <= scsiPrice && tmpPower <= scsiPower) {                	 
                     double performance = computePerformance(data, ssdLunIndex, scsiLunIndex, sortedLUNs, in.get_orderingParameter());                            
                     double price = computePrice(amountSATA, amountSCSI, amountSSD, data);
                     double power = computePowerConsumption(amountSATA, amountSCSI, amountSSD, data);

                     if (performance <= scsiPer && price <= scsiPrice && power <= scsiPower &&
                    		 performance <= perfLim && price <= priceLim && power <= powerLim) {
                         vecResult.add(new ConfigurationGraphObject(performance, power, price, amountSSD, amountSCSI, amountSATA));
                     }
                 }
        	}       	        	
        }
        
        ConfigurationGraphData out = new ConfigurationGraphData(vecResult);
        System.out.println("compute configuration ended: " + GregorianCalendar.getInstance().getTime());
        return out;
    }
    
    
    private void memoization(Data data, int[] sortedLuns, OrderingParameters orderingParameter, Calendar start, Calendar end){    	
    	int numtu=data.getNumOfTimeUnits();
    	//System.out.println(start.get(Calendar.DAY_OF_MONTH)+"."+(start.get(Calendar.MONTH)+1)+"."+start.get(Calendar.YEAR)+" ,"+start.get(Calendar.HOUR_OF_DAY)+":"+start.get(Calendar.MINUTE)+":"+start.get(Calendar.SECOND));
    	tustart=-1;
    	tuend=-1;
    	for( int i=0; i<numtu; i++){
    		TimeUnit tu = data.getTimeUnit(i);
    		Calendar c = tu.getStart();
    		Calendar e = (GregorianCalendar)c.clone();
    		e.add(Calendar.SECOND, tu.get_timesize());
    		if (c.after(start) && e.before(end)){
    			if(tustart == -1)
    				tustart = i;
    		}
    		else if(tustart!=-1 && tuend==-1){
    			tuend=i;
    		}
    	}
    	if(tustart!=-1 && tuend==-1)
    		tuend=numtu;
    	
    	
    	iokbs = new double[tuend-tustart][sortedLuns.length];
    	ionums = new double[tuend-tustart][sortedLuns.length];
    	sizes = new double[sortedLuns.length];
    	
    	
    	for(int j=0; j<sortedLuns.length; j++){
    		if(j==0)
    			sizes[0]=data.getLUN(sortedLuns[0]).get_size();
    		else
    			sizes[j]=sizes[j-1]+data.getLUN(sortedLuns[j]).get_size();
    	}
    	for(int i=tustart; i<tuend; i++){
    		LUNPerTimeUnit[] allLptu = data.getAllLunPerTimeUnits(i, 0, sortedLuns.length - 1 , orderingParameter);
    		for(int j=0; j<sortedLuns.length; j++){
    			LUNPerTimeUnit lptu = allLptu[j];
    			double tmpNumIo = lptu.get_numOfReads() + lptu.get_numOfWrites();
    			if (tmpNumIo > 0.1) {
    				if(j==0){
    					iokbs[i-tustart][0]=lptu.get_totalKbRead()+lptu.get_totalKbWritten();
    					ionums[i-tustart][0]=tmpNumIo;    					
    				}
    				else{
    					iokbs[i-tustart][j]=iokbs[i-tustart][j-1]+lptu.get_totalKbRead()+lptu.get_totalKbWritten();
    					ionums[i-tustart][j]=ionums[i-tustart][j-1]+tmpNumIo;    					
    				}
    			}
    			else{
    				if(j==0){
    					iokbs[i-tustart][0]=0;
    					ionums[i-tustart][0]=0;    					
    				}
    				else{
    					iokbs[i-tustart][j]=iokbs[i-tustart][j-1];
    					ionums[i-tustart][j]=ionums[i-tustart][j-1];    
    				}
    			}
    		}
    	} 	
    }
    

	private int getMaxDisks(int start, double diskSize) {
    	double size;
    	if(start==0)
    		size= sizes[sizes.length-1]/diskSize;
    	else size= (sizes[sizes.length-1]-sizes[start-1])/diskSize;
    	if((int)size< size)
        	size++;
        return (int)size;

	}



    private double computePrice(double sataAmount, double scsiAmount, double ssdAmount, Data data) {
        double price = sataAmount * Data.SATAPRICE + scsiAmount * Data.SCSIPRICE + ssdAmount * Data.SSDPRICE;
        return price;
    }

    private double computePowerConsumption(double sataAmount, double scsiAmount, double ssdAmount, Data data) {
        double power = sataAmount * Data.SATAPOWER + scsiAmount * Data.SCSIPOWER + ssdAmount * Data.SSDPOWER;
        return power;
    }

    private double computePerformance(Data data, int indexSSD, int indexSCSI, int[] sortedLUNS, OrderingParameters orderingParameter) {

        double iokb, ionum, time, e, u;
        TimeUnit tu;
        double totalionum = 0;
        double SSDTotalionum = 0;
        double SCSITotalionum = 0;
        double SATATotalionum = 0;

        double performance = 0;
        double SSDPerformance = 0;
        double SCSIPerformance = 0;
        double SATAPerformance = 0;

        //SSD
        if (indexSSD != 0) {
            for (int i = tustart; i < tuend; i++) {
                tu = data.getTimeUnit(i);                
                iokb = 0;
                ionum = 0;
                iokb=iokbs[i-tustart][indexSSD-1];
                ionum=ionums[i-tustart][indexSSD-1];
                SSDTotalionum += ionum;
                time = tu.get_timesize() * amountSSD;
                e = (iokb / Data.SSDTHROUGHPUT) + (ionum * Data.SSDLATENCY); 
                u = e / time;
                if (u > 0.8) {
                    double tmpAmountSSd = e/(tu.get_timesize()*0.8);
                    amountSSD=(int)(e/(tu.get_timesize()*0.8));
                    if (amountSSD<tmpAmountSSd)
                        amountSSD++;
                    i = tustart;
                    SSDPerformance = 0;
                    SSDTotalionum = 0;
                } else {
                    SSDPerformance += e / (1 - u);
                }
                //this check saves unnecessary checks (of configurations that their price is too high)
                if (amountSSD * Data.SSDPRICE > priceLim) {
                    return -1;
                }
            }
        }

        //SCSI
        if (indexSSD != indexSCSI) {
            for (int i = tustart; i < tuend; i++) {
                tu = data.getTimeUnit(i);
                iokb = 0;
                ionum = 0;
                if(indexSSD!=0){
                	iokb=iokbs[i-tustart][indexSCSI-1]-iokbs[i-tustart][indexSSD-1];
                	ionum=ionums[i-tustart][indexSCSI-1]-ionums[i-tustart][indexSSD-1];
                }
                else{
                	iokb=iokbs[i-tustart][indexSCSI-1];
                	ionum=ionums[i-tustart][indexSCSI-1];
                }
                SCSITotalionum += ionum;
                time = tu.get_timesize() * amountSCSI; //serialize time
                e = (iokb / Data.SCSITHROUGHPUT) + (ionum * Data.SCSILATENCY);//amount of work done
                u = e / time; 
                if (u > 0.8) {
                    double tmpAmountScsi = e/(tu.get_timesize()*0.8);
                    amountSCSI=(int)(e/(tu.get_timesize()*0.8));
                    if (amountSCSI<tmpAmountScsi)
                        amountSCSI++;
                    i = tustart;
                    SCSIPerformance = 0;
                    SCSITotalionum = 0;
                } else {
                    SCSIPerformance += e / (1 - u);
                }
            }
            //this check saves unnecessary checks (of configurations that their price is too high)
            if (amountSCSI * Data.SCSIPRICE > priceLim) {
                return -1;
            }
        }

        //SATA
        if (indexSCSI < sortedLUNS.length - 1) {
            for (int i = tustart; i < tuend; i++) {
                tu = data.getTimeUnit(i);
                iokb = 0;
                ionum = 0;
                if(indexSCSI!=0){
                	iokb=iokbs[i-tustart][sortedLUNS.length-1]-iokbs[i-tustart][indexSCSI-1];
                	ionum=ionums[i-tustart][sortedLUNS.length-1]-ionums[i-tustart][indexSCSI-1];
                }
                else{
                	iokb=iokbs[i-tustart][sortedLUNS.length-1];
                	ionum=ionums[i-tustart][sortedLUNS.length-1];
                }
                SATATotalionum += ionum;
                time = tu.get_timesize() * amountSATA;
                e = (iokb / Data.SATATHROUGHPUT) + (ionum * Data.SATALATENCY);
                u = e / time;             
                if (u > 0.8) {
                    double tmpAmountSATA = e/(tu.get_timesize()*0.8);
                    amountSATA=(int)(e/(tu.get_timesize()*0.8));
                    if (amountSATA<tmpAmountSATA)
                        amountSATA++;
                    i = tustart;
                    SATAPerformance = 0;
                    SATATotalionum = 0;
                } else {
                    SATAPerformance += e / (1 - u);
                	}
                //this check saves unnecessary checks (of configurations that their price is too high)
                if (amountSATA * Data.SATAPRICE > priceLim) {
                    return -1;
                }
            }
        }

        performance = SSDPerformance + SCSIPerformance + SATAPerformance;
        totalionum = SSDTotalionum + SCSITotalionum + SATATotalionum;
        if (totalionum > 0) {
            performance = performance / totalionum;
        }

        BigDecimal bd = new BigDecimal(performance);
        bd = bd.setScale(10, BigDecimal.ROUND_HALF_UP);


        return bd.doubleValue();

    }

    


}
